package legato.injection
{
    import flash.utils.getQualifiedClassName;
    
    import mx.utils.ObjectUtil;
    
    
    /**
     * Used for creating application components with depedency injection pattern.
     * These components are used by <code>InjectionContainer</code> for configuration,
     * creation and serving component implementations.
     * @author Piotr
     * @see InjectionContainer
     * @see IDIComponent
     */
    public dynamic class DIComponent implements IDIComponent
    {
        
        /**
         * Use it for creation of new instances instead of constructor.
         * @param scope component scope; can be : "application", "context" or "instance"
         * @param componentClass the class of component
         * @param setterTemplate setter template object
         * @param constructorParams array of constructor parameters
         * @return new instance of DIComponent
         *
         */
        public static function create(scope:String, componentClass:Class, setterTemplate:Object, constructorParams:Array = null):DIComponent
        {
            var newDIComponent:DIComponent = new DIComponent();
            newDIComponent.scope = scope;
            newDIComponent.componentClass = componentClass;
            newDIComponent.setterTemplate = setterTemplate;
            newDIComponent.constructorParams = constructorParams;
            return newDIComponent;
        }
        
        private var _scope:String = "application";
        
        private var _setterTemplate:Object;
        
        private var _constructorParams:Array;
        
        private var _componentClass:Class;
        
        private var nullContextObject:Object;
        
        private var contextMap:Array;
        
        [Inspectable(enumeration="application,context,instance")]
        /**
         * Component scope. One of <i>application</i>, <i>context</i> or <i>instance</i>.
         * See <code>DIComponentScope</code> for more information.<br/>
         * Implementation of <code>IDIComponent</code>.
         * @return
         *
         */
        public function get scope():String
        {
            return _scope;
        }
        
        public function set scope(val:String):void
        {
            if (val != null &&
                val != DIComponentScope.APPLICATION_SCOPE &&
                val != DIComponentScope.CONTEXT_SCOPE &&
                val != DIComponentScope.INSTANCE_SCOPE)
            {
                throw new Error("Wrong 'scope' value : " + val + ". Permitted values are: " + DIComponentScope.APPLICATION_SCOPE + ", "
                                + DIComponentScope.CONTEXT_SCOPE + " and " + DIComponentScope.INSTANCE_SCOPE);
            }
            else
            {
                _scope = val != null ? val : DIComponentScope.APPLICATION_SCOPE;
            }
        
        }
        
        /**
         * object, that contains attributes to pass to newly created component instance
         * @return
         *
         */
        public function get setterTemplate():Object
        {
            return _setterTemplate;
        }
        
        public function set setterTemplate(val:Object):void
        {
            this._setterTemplate = ObjectUtil.copy(val);
        }
        
        /**
         * Array of construcor parameters. Due to ActionScript limitations up to 15 parameters are allowed.
         * @return
         *
         */
        public function get constructorParams():Array
        {
            return _constructorParams;
        }
        
        public function set constructorParams(val:Array):void
        {
            _constructorParams = val;
        }
        
        /**
         * Class of componenet.
         * @return
         *
         */
        public function get componentClass():Class
        {
            return _componentClass;
        }
        
        public function set componentClass(val:Class):void
        {
            _componentClass = val;
        }
        
        /**
         * Creates new instance of component in given context.
         * Context parameter is used only witch <code>scope</code> set to <i>context</i>.
         * You can use <code>null</code> context - works like <i>application</i> scope.<br/>
         * Implementation of <code>IDIComponent</code>
         * @param context name of the context
         * @return component instance
         *
         */
        public function getInstance(context:String = null):Object
        {
            if (this.scope == DIComponentScope.APPLICATION_SCOPE || context == null)
            {
                if (this.nullContextObject == null)
                {
                    this.nullContextObject = createNewInstance(context);
                }
                return this.nullContextObject;
            }
            else if (this.scope == DIComponentScope.CONTEXT_SCOPE)
            {
                if (this.contextMap == null)
                {
                    this.contextMap = new Array();
                }
                
                if (this.contextMap[context])
                {
                    return this.contextMap[context];
                }
                else
                {
                    var newInstance:Object = this.createNewInstance(context);
                    this.contextMap[context] = newInstance;
                    return newInstance;
                }
                
            }
            else if (this.scope == DIComponentScope.INSTANCE_SCOPE)
            {
                return this.createNewInstance(context);
            }
            return null;
        }
        
        public function removeInstance(context:String = null):void
        {
            if (this.scope == DIComponentScope.APPLICATION_SCOPE || context == null)
            {
                this.nullContextObject = null;
            }
            else if (this.scope == DIComponentScope.CONTEXT_SCOPE)
            {
                if (this.contextMap != null && this.contextMap[context])
                {
                  	this.contextMap[context] = null;
                }
            }
        }
        
        /**
         * Creates new instance of component using constructor parameters, setter template and dunamic properties.
         * Dynamic properties are joined with setter template, eventually they can override properties in setter template
         *
         * @return created component
         *
         */
        private function createNewInstance(context:String):Object
        {
            var createdComponent:Object = null;
            
            if (this.componentClass != null)
            {
                //Use setter template first
                var propertiesObject:Object = this._setterTemplate;
                
                if (propertiesObject == null)
                {
                    propertiesObject = new Object();
                }
                
                //Dynamic properties overrides setter template
                for (var i:String in this)
                {
                    propertiesObject[i] = this[i];
                }
                
                var resolvedConstructorParams:Array = new Array();
                
                for (i in this.constructorParams)
                {
                    if (this.constructorParams[i] is IDIComponent)
                    {
                        resolvedConstructorParams[i] = IDIComponent(this.constructorParams[i]).getInstance(context);
                    }
                    else
                    {
                        resolvedConstructorParams[i] = this.constructorParams[i];
                    }
                }
                
                createdComponent = this.pseudoConstructorInjection(resolvedConstructorParams);
                
                for (i in propertiesObject)
                {
                    if (propertiesObject[i] is IDIComponent)
                    {
                        createdComponent[i] = IDIComponent(propertiesObject[i]).getInstance(context);
                    }
                    else
                    {
                        createdComponent[i] = propertiesObject[i];
                    }
                }
                
                MetaInjecter.configure(createdComponent, context);
                
            }
            else
            {
                throw new Error("componentsClass is null object reference in DIComponent ");
            }
            return createdComponent;
        }
        
        /**
         * Resolves parameters. It's the injection part implementation.
         * @param paramsArray
         * @param context
         * @return
         *
         */
        private function resolveParameters(paramsArray:Array, context:String):Array
        {
            var newParams:Array = new Array();
            
            for (var i:String in paramsArray)
            {
                trace(i + " : " + (typeof paramsArray[i]) + " : " + (paramsArray[i] is IDIComponent));
                
                if (paramsArray[i] is IDIComponent)
                {
                    newParams[i] = IDIComponent(paramsArray[i]).getInstance(context);
                }
                else
                {
                    newParams[i] = paramsArray[i];
                }
                
            }
            return newParams;
        }
        
        /**
         * Implementation of pseudo construcor injection. Up to 15 parameters are allowed.
         * @param params
         * @return
         *
         */
        private function pseudoConstructorInjection(params:Array):Object
        {
            var instance:Object;
            
            if (params == null || params.length < 1)
            {
                instance = new this.componentClass;
            }
            else if (params.length > 15)
            {
                throw new Error("Unable to instantiate " + getQualifiedClassName(this.componentClass) + " with constructor injection for more than 15 parameters. Use setter injection");
            }
            else
            {
                switch (params.length)
                {
                    case 0:
                        instance = new this.componentClass();
                        break;
                    case 1:
                        instance = new this.componentClass(params[0]);
                        break;
                    case 2:
                        instance = new this.componentClass(params[0], params[1]);
                        break;
                    case 3:
                        instance = new this.componentClass(params[0], params[1], params[2]);
                        break;
                    case 4:
                        instance = new this.componentClass(params[0], params[1], params[2], params[3]);
                        break;
                    case 5:
                        instance = new this.componentClass(params[0], params[1], params[2], params[3], params[4]);
                        break;
                    case 6:
                        instance = new this.componentClass(params[0], params[1], params[2], params[3], params[4], params[5]);
                        break;
                    case 7:
                        instance = new this.componentClass(params[0], params[1], params[2], params[3], params[4], params[5], params[6]);
                        break;
                    case 8:
                        instance = new this.componentClass(params[0], params[1], params[2], params[3], params[4], params[5], params[6], params[7]);
                        break;
                    case 9:
                        instance = new this.componentClass(params[0], params[1], params[2], params[3], params[4], params[5], params[6], params[7], params[8]);
                        break;
                    case 10:
                        instance = new this.componentClass(params[0], params[1], params[2], params[3], params[4], params[5], params[6], params[7], params[8], params[9]);
                        break;
                    case 11:
                        instance = new this.componentClass(params[0], params[1], params[2], params[3], params[4], params[5], params[6], params[7], params[8], params[9], params[10]);
                        break;
                    case 12:
                        instance = new this.componentClass(params[0], params[1], params[2], params[3], params[4], params[5], params[6], params[7], params[8], params[9], params[10], params[11]);
                        break;
                    case 13:
                        instance = new this.componentClass(params[0], params[1], params[2], params[3], params[4], params[5], params[6], params[7], params[8], params[9], params[10], params[11], params[12]);
                        break;
                    case 14:
                        instance = new this.componentClass(params[0], params[1], params[2], params[3], params[4], params[5], params[6], params[7], params[8], params[9], params[10], params[11], params[12],
                                                           params[13]);
                        break;
                    case 15:
                        instance = new this.componentClass(params[0], params[1], params[2], params[3], params[4], params[5], params[6], params[7], params[8], params[9], params[10], params[11], params[12],
                                                           params[13], params[14]);
                        break;
                
                }
            }
            
            return instance;
        }
    
    }
}